Skip to content

[poc]lightweight plugin system to the Eclipse Che Dashboard - #1552

Open
olexii4 wants to merge 28 commits into
mainfrom
plugins
Open

[poc]lightweight plugin system to the Eclipse Che Dashboard#1552
olexii4 wants to merge 28 commits into
mainfrom
plugins

Conversation

@olexii4

@olexii4 olexii4 commented May 4, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Add lightweight plugin system to the Eclipse Che Dashboard.

It lets optional features be developed, versioned, and distributed
independently of the dashboard core, without forking the main repository.

The system has three parts:

  1. packages/dashboard-plugins — shared type package and runtime registry
    (the "plugin SDK"). Published as @eclipse-che/dashboard-plugins.
  2. Plugin bundles — independent source trees in a separate repository
    (olexii4/che-dashboard-plugins),
    released as ZIP archives on GitHub Releases.
  3. Dashboard integration — a thin wiring layer (scripts/fetch-plugins.sh,
    scripts/prepare-plugins.sh, src/plugins/) that downloads and mounts
    plugins into the dashboard build.

packages/dashboard-plugins — the Plugin SDK

Purpose

Defines the TypeScript contracts (types, interfaces) that both the dashboard
core and plugin authors depend on. Also provides the runtime plugin registry
used by the frontend at startup.

No business logic lives here. The package exists purely to establish a
stable API surface between the dashboard and its plugins.

Package contents

packages/dashboard-plugins/
├── src/
│   ├── types.ts         # All plugin interfaces (FrontendPlugin, PluginSlots, …)
│   ├── registry.ts      # Runtime plugin registration and retrieval
│   ├── index.ts         # Public re-exports
│   └── frontend/
│       └── PluginSlot.tsx  # React slot component + slot getter functions

Type reference

PluginManifest

interface PluginManifest {
  id: string;          // unique plugin identifier
  name: string;
  version: string;
  description: string;
  enabled: boolean;    // if false, plugin is ignored at registration time
}

FrontendPlugin

interface FrontendPlugin {
  manifest: PluginManifest;
  reducerKey: string;          // key in the Redux root reducer
  reducer: Reducer;            // Redux reducer for plugin state
  bootstrap?: (store: Store) => Promise<void>;  // called once after store init
  slots: PluginSlots;
  workspaceHooks?: WorkspaceHooks;
}

PluginSlots — extension points

Slot Type Where rendered
workspaceCreation ComponentType Create Workspace page
workspaceDetailsOverview ComponentType Workspace Details — Overview tab
workspacesListColumn ColumnDefinition Workspaces list table — extra column
userPreferencesTab TabDefinition User Preferences — extra tab
factoryParams FactoryParamExtension Factory URL parameter parsing
navigationItems NavigationItemDefinition[] Sidebar navigation
loaderTabs LoaderTabDefinition[] Starting workspace — loader tabs

NavigationItemDefinition

interface NavigationItemDefinition {
  to: string;                               // route path, e.g. '/devfiles'
  label: string;                            // static label fallback
  labelSelector?: (state: unknown) => string; // dynamic label from Redux state
  insertAfter?: string;                     // to-path of the item to insert after
}

insertAfter controls ordering without the plugin knowing the full nav list.
Example: insertAfter: '/create-workspace' places "Devfiles" immediately after
"Create Workspace".

LoaderTabDefinition

interface LoaderTabDefinition {
  key: string;          // tab eventKey, e.g. 'DevWorkspace'
  title: string;        // tab label
  component: ComponentType<{ workspace: unknown; isActive: boolean }>;
  insertAfter?: string; // key of the tab to insert after
}

WorkspaceHooks

interface WorkspaceHooks {
  // Called when a workspace is created — lets a plugin inject DevWorkspace patches
  onWorkspaceCreate?: (workspace, factoryParams) => workspace;
  // Called when a workspace is started — lets a plugin block or mutate the start
  onWorkspaceStart?: (workspace) => workspace | null;
}

Registry API (used by the dashboard core)

// Register on startup (called from src/plugins/index.ts)
registerFrontendPlugin(plugin: FrontendPlugin): void

// Read at render time (called by Navigation, Loader, UserPreferences, etc.)
getRegisteredFrontendPlugins(): FrontendPlugin[]
getPluginNavigationItems(): NavigationItemDefinition[]
getPluginLoaderTabs(): LoaderTabDefinition[]
getPluginTabs(): TabDefinition[]       // User Preferences tabs
getPluginColumns(): ColumnDefinition[] // Workspaces list columns

Screenshot/screencast of this PR

What issues does this PR fix or reference?

Is it tested? How?

Release Notes

Docs PR

@olexii4
olexii4 requested review from akurinnoy and ibuziuk as code owners May 4, 2026 00:47
@che-bot

che-bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Click here to review and test in web IDE: Contribute

@openshift-ci

openshift-ci Bot commented May 4, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: olexii4

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@olexii4 olexii4 changed the title [poc] lightweight plugin system to the Eclipse Che Dashboard [poc][wip] lightweight plugin system to the Eclipse Che Dashboard May 4, 2026
@olexii4
olexii4 marked this pull request as draft May 4, 2026 00:57
@olexii4
olexii4 force-pushed the plugins branch 6 times, most recently from fffca8b to 48b1f58 Compare May 4, 2026 13:37
@olexii4
olexii4 force-pushed the plugins branch 11 times, most recently from a4c8897 to 0fb931b Compare May 5, 2026 13:40
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Docker image build succeeded: quay.io/eclipse/che-dashboard:pr-1552 (linux/amd64, linux/arm64)

kubectl patch command
kubectl patch -n eclipse-che "checluster/eclipse-che" --type=json -p="[{"op": "replace", "path": "/spec/components/dashboard/deployment", "value": {containers: [{image: "quay.io/eclipse/che-dashboard:pr-1552", name: che-dashboard}]}}]"

1 similar comment
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

Docker image build succeeded: quay.io/eclipse/che-dashboard:pr-1552 (linux/amd64, linux/arm64)

kubectl patch command
kubectl patch -n eclipse-che "checluster/eclipse-che" --type=json -p="[{"op": "replace", "path": "/spec/components/dashboard/deployment", "value": {containers: [{image: "quay.io/eclipse/che-dashboard:pr-1552", name: che-dashboard}]}}]"

@olexii4
olexii4 force-pushed the plugins branch 2 times, most recently from 9d85cce to a660342 Compare May 6, 2026 16:27
olexii4 added 26 commits July 20, 2026 16:25
- overrides.css: WCAG colour contrast improvements; label-required styling
  via global danger colour token; dark theme label contrast overrides
- DevfileViewer: refactor to @uiw/react-codemirror with useMemo theming;
  remove obsolete snapshot; update test to use direct unit testing
- BackupStatusBadge, ExpandableWarning: CSS module and component fixes
- BasicViewer, ResourceIcon, Workspace/Status: minor CSS adjustments
- websocketClient: add CONFIGMAP channel subscription helpers
- WorkspacesList: AI tool column via plugin slot; Rows.tsx update
- UserPreferences/GitConfig/Form/SectionUser: validation improvements
- backend schemas.ts: add DevWorkspace and Devfile schema route constants
- patchOptions.ts, restParams.ts, backend package.json: minor additions

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
scripts/fetch-plugins.sh (new):
- Downloads plugin ZIPs from GitHub Releases for a given repo and ref
- PLUGINS_LIST unset: queries Releases API, installs all plugins found
- PLUGINS_LIST set (JSON array): installs only the listed plugins
- LOCAL_PLUGINS: skips download, copies from a local checkout instead
- Uses read loop instead of mapfile for Alpine sh / POSIX compatibility

scripts/prepare-plugins.sh (new):
- Wires fetched plugin files into the dashboard source tree
- Generates packages/dashboard-frontend/src/plugins/index.ts

build/dockerfiles/Dockerfile:
- ARG PLUGINS_REPO, PLUGINS_REF, PLUGINS_LIST to control which plugins
  are fetched and from where
- Fetches and prepares plugins before yarn build:packages
- PLUGINS_LIST unset = install all; JSON array = install subset

run/local-patch.sh (new): helper for local development patching

.deps: update dependency declarations
.gitignore: ignore plugins/ directory and generated index

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
- dashboard-plugins/package.json: add --passWithNoTests so jest exits
  cleanly when no test files exist yet in the SDK package
- Layout/Sidebar/__tests__: mock NavigationAgentList; update snapshot
  to reflect the new mainNavWrapper + agentNavWrapper sidebar layout
- pages/GetStarted/__tests__: update snapshot for PluginSlot spacer
- services/bootstrap/__tests__: mock subscribeToAgentPodChanges and
  requestAiAgentRegistry; update imports to plugin symlink paths

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Plugin routes agents.ts, devfiles.ts now use relative imports for
helpers that were moved into the plugin repository, fixing the webpack
'Can't resolve @/routes/api/helpers/*' build errors.

Fix is in olexii4/che-dashboard-plugins@9d7c0b2

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
The private dispatch<T>() helper returns T but TypeScript cannot prove
T is a Promise at the call site, causing TS2571 ('Object is of type
unknown') when .catch() is chained. Revert to the explicit 3-argument
thunk invocation for the one case that needs .catch() on the result.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
tabOrder.indexOf() previously rejected string when activeTabKey was
widened to UserPreferencesTab | string. Cast tabOrder to
ReadonlyArray<string> for the indexOf call so plugin-registered tab keys
(plain strings) are accepted.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
- EXCLUDED/prod.md: remove 10 stale entries for packages no longer in
  production deps (moved to plugins repo); add
  @eclipse-che/common@workspace:packages/common (local workspace package,
  unresolvable by dash-licenses)
- EXCLUDED/dev.md: remove 2 stale entries; add
  @eclipse-che/license-tool@2.0.0 (dev tool, unresolvable by dash-licenses)
- prod.md / dev.md: regenerate from current yarn.lock

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Update @eclipse-che/common version reference in yarn.lock from
7.117.0-next to 7.121.0-next, update dashboard-plugins package.json
to match, and regenerate .deps/ license files with new unresolved
dependency entries.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Two copies of these interfaces were merged during rebase — one from
main (strict literal types) and one from the plugins branch (wider
optional types). Remove the second copy and merge its extra fields
(description, docsUrl, icon) into the canonical first declaration.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Two copies of aiProviderKeyBodySchema/aiProviderKeyParamsSchema were
left in schemas.ts after the rebase, and two sets of aiConfig/aiRegistry
route imports were left in app.ts (one pointing to plugin paths, one to
the old @/routes/api paths). Remove the duplicates.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
…sts from tsc

Remove fetchAiRegistry/fetchAiProviderKeyStatus from bootstrap (they
referenced aiConfigActionCreators/selectAiConfigEnabled which no longer
exist in the plugins branch - they were replaced by bootstrapPlugins).

Exclude src/plugins/**/__tests__ from the main tsconfig so that external
plugin test files (fetched at build time via symlinks) are not compiled
by ts-loader during the production webpack build. Plugin tests are still
run by jest with its own transform config.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Regenerate .deps/prod.md and .deps/dev.md to reflect the current
dependency state after the rebase.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
The plugin path imports for registerAiConfigRoutes and
registerAiRegistryRoute caused test failures: the plugin versions
do not integrate cleanly with the existing jest mock infrastructure
(getDevWorkspaceClient mock), returning 500 in tests.

The local @/routes/api/aiConfig and @/routes/api/aiRegistry files
are still present, fully functional, and correctly tested. Keep using
them for these two routes; the new plugin routes (agents, devfiles,
aiAgentRegistry, devfileSchema) remain plugin-sourced.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Three changes to fix the 44+ failing test suites:

1. sanitize-html ESM stub — sanitize-html@2.17.x ships as ESM; add a
   CJS stub in __mocks__/sanitize-html.js and wire it via moduleNameMapper.
   The stub implements real tag-stripping logic so BannerAlertBranding's
   sanitization assertion passes.

2. Plugin module stub — @/plugins/* paths are symlinked at build time by
   prepare-plugins.sh and don't exist locally. Add __mocks__/pluginStub.tsx
   and map all @/plugins/* imports to it so jest.mock() calls in tests can
   resolve the module. Plugin mapper placed before @/ mapper so it wins.

3. Suppress TS2307 in ts-jest — TypeScript still tries to resolve plugin
   paths through tsconfig even when moduleNameMapper redirects them. Set
   diagnostics.ignoreCodes: [2307] to suppress "Cannot find module" for
   these expected-absent symlinks.

Also update the Sidebar snapshot to include the agentNavWrapper nav that
was added to the sidebar during the plugins integration.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Three issues caused 44 test suites and 29 tests to fail:

1. sanitize-html ESM import — sanitize-html@2.17.x ships as ESM; add a
   CJS stub in __mocks__/sanitize-html.js with real tag-stripping logic
   (so BannerAlertBranding sanitization assertion still passes). Wired
   via moduleNameMapper with higher priority than the @/ mapper.

2. Plugin module resolution — @/plugins/* paths are symlinked at build
   time by prepare-plugins.sh and absent locally. Add a universal proxy
   stub in __mocks__/pluginStub.tsx mapped to all @/plugins/* imports.
   The proxy handles three module shapes:
   - *Reducer exports → identity reducer (Redux requires a function)
   - *ActionCreators exports → spyable proxy-object (bootstrap test spyOn)
   - Everything else → stub thunk function
   TS2307 "Cannot find module" suppressed in ts-jest diagnostics since
   plugin paths are intentionally absent in local dev.

3. aiConfig initial state — rootReducer now sources aiConfigReducer from
   the plugin stub (returns {} not { providers:[], tools:[], ... }).
   MockStoreBuilder pre-seeds aiConfig with the expected initial shape so
   selectors like selectAiTools return [] rather than undefined.

Also:
- Use a factory mock for NavigationAgentList in Sidebar test (auto-mock
  of a Proxy returns an object, not a callable component).
- Update Sidebar snapshot to reflect the agentNavWrapper added during
  plugin integration.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
…ettier

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
The plugin's requestAiAgentRegistry is a plain action creator (not a
thunk creator), so calling it as requestAiAgentRegistry()(dispatch, …)
throws "is not a function". Use store.dispatch(...) instead, which works
for both thunk and plain action creators.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Use this.dispatch() helper with explicit AppThunk<Promise<void>> cast
so TypeScript accepts the plugin action creator return type regardless
of whether the plugin stub or the real module is in scope.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
bootstrapPlugins already handles plugin initialization including the
AI agent registry fetch. Calling requestAiAgentRegistry separately
fails because the plugin's action creator is not a thunk creator.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
Regenerate .deps/ files to incorporate tar/js-yaml CVE fix from main
and resolve newly unresolved prod/dev dependencies in EXCLUDED lists.

Assisted-by: Claude Sonnet 4.6
Signed-off-by: Oleksii Orel <oorel@redhat.com>
@github-actions

Copy link
Copy Markdown

Docker image build succeeded: quay.io/eclipse/che-dashboard:pr-1552 (linux/amd64, linux/arm64, linux/s390x)

kubectl patch command
kubectl patch -n eclipse-che "checluster/eclipse-che" --type=json -p="[{"op": "replace", "path": "/spec/components/dashboard/deployment", "value": {containers: [{image: "quay.io/eclipse/che-dashboard:pr-1552", name: che-dashboard}]}}]"

@openshift-ci

openshift-ci Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants